Skip to content

Revive webmcp dead features - #1

Merged
theprogrammersingh merged 8 commits into
mainfrom
revive-webmcp-dead-features
Aug 29, 2026
Merged

Revive webmcp dead features#1
theprogrammersingh merged 8 commits into
mainfrom
revive-webmcp-dead-features

Conversation

@theprogrammersingh

Copy link
Copy Markdown
Owner

No description provided.

…encies

Progress.md marked three headline features DEAD — code that exists, passes
tests, and has no caller, so it did nothing in the running app. All three are
PRD §7 checklist rows and all three were one wiring problem each.

**State-gated `approve_expense`.** `Session.refreshPendingApprovals()` was never
called, so the count stayed 0, `ToolSession`'s gate never opened, and the tool
never registered. The shell now polls on sign-in and after any mutating call.
Reads are excluded deliberately: `search_expenses` runs on almost every Copilot
question, and polling on it would be a request per question for a count that
cannot have changed.

**`tool_call_log`.** The table, repository, route and Settings viewer all
existed; nothing wrote to them, so the viewer showed seed rows forever. New
`ToolCallAudit` POSTs every invocation. It never rejects and never blocks —
this runs inside a tool's own execution path, and an audit trail that can fail
the action it describes is worse than no audit trail. Payloads over 4KB are
truncated with a marker, because a silently shortened row reads as the whole
call. Add Expense logs itself, since the declarative form is the only tool call
a human can make and is what gives the viewer's Human filter anything to show.

**Cross-origin tools.** `discoverRemoteTools()` had no caller — and could not
have worked if it had, because the partner page was served from Actuo's own
origin, so `getTools()` marked its tools same-origin and the Copilot filtered
every one of them out. The demo needs a genuinely second origin, so
`scripts/partner-server.mjs` (zero dependencies) serves `frontend/public` on
:4201 and `pnpm run dev` starts it as a third pane. The embedded origin comes
from `PARTNER_DEMO_ORIGIN` via `GET /api/config`, so a deploy changes it
without a rebuild. New `/agent` page embeds it with `allow="tools"`, and also
gives `discoveredTools()` and `invocationLog()` their first consumer. Without
the Chrome flag it says what is missing rather than showing an empty list, and
every other tool keeps working.

All three hang off one seam: `ToolRegistry.observe()`. `log()` is already the
single point every invocation passes through — the Copilot's, an external
agent's, and cross-origin ones — so a subscriber cannot miss a caller. It is a
callback rather than an injected service so the registry stays free of HTTP and
session dependencies and its spec needs no fakes. A throwing observer is caught.

**Mixed currencies.** `converted_amount` is written only when the expense is
already in the base currency, and there is no FX pass, so it is null for every
foreign row — while every rollup fell back to the raw `amount`. The seed data
has INR, USD and EUR, so the dashboard was adding dollars to rupees at 1:1: a
$200 charge counted as ₹200, and the Expenses table printed it under a ₹ symbol.
Now a row counts only when it has a base-currency value, and the rest are
counted and stated — `sumSpend()` returns `{total, excluded}`, `sumByCategory()`
returns `unconverted`, and that surfaces as `BudgetStatus.unconvertedCount`, a
muted line on the dashboard and budgets screens, and a field in the
`get_budget_status` result so the Copilot can qualify the figure instead of
reporting a partial total as a complete one. This is the honest interim, not the
feature: when real FX starts filling `converted_amount` those rows re-enter
every total with no code change.

Verified live in Chrome 151 with the WebMCP flag: `approve_expense` present as
owner with 3 pending and absent as member (and the API returns 403 for that
member either way); an agent row and a human row both landing in `tool_call_log`;
both partner tools discovered from localhost:4201 with `executeTool()` returning
a price cross-origin; the dashboard totalling INR only and naming the two rows
it left out; light and dark both legible.

Not verified: the mobile tab bar at an actual 390px viewport — the automation
viewport is pinned, so the six labels were confirmed by measurement (widest is
"Dashboard" at ~54px in a 65px slot). Hard-navigating to `/agent` 302s to
/login, but that is pre-existing and affects every gated route: the server
routes prerender `**`, so `authGuard` runs with no session. Both are recorded
in Progress.md.
…alling back to CSR

Phase 0's last unbuilt item and §12's first checkbox. The design was already
decided and written down — only the file was missing. `bootstrap.ts` exports
`createNestApp()` so `main.ts` and `server.mjs` cannot diverge on routing, and
`app.module.ts` resolves `.env` from the module rather than cwd specifically
because "in the combined production deploy the entry point is `node server.mjs`
at the repo root". That file now exists.

**The composition is five lines.** The built Angular SSR bundle turns out to be
fully self-contained — its only imports are `node:` builtins, Express is bundled
in — and `createNodeRequestHandler` returns its argument unchanged, so the
exported `reqHandler` *is* the Express app. So `server.mjs` appends it to Nest's
own Express instance after Nest's routes. No new dependency; the repo root's
`node_modules` stays at two dev packages. It works only because
`setGlobalPrefix('/api')` scopes Nest's not-found router instead of installing a
global catch-all — the exact property `routing-contract.e2e-spec.ts` exists to
pin, now load-bearing from both sides.

**SSR was silently disabled, and only running the thing showed it.** Angular 21
checks the `Host` header against an allowlist for SSRF, and `angular.json` had
`allowedHosts: []`. Off the list it does not error: it falls back to client-side
rendering and logs a line nobody reads. So every public page would have shipped
as CSR, quietly discarding the SSR, structured data and Lighthouse work in §8.5 —
and Angular say it becomes a hard 400 in a future major. `angular.json` now lists
localhost so `node server.mjs` renders locally, and `NG_ALLOWED_HOSTS` in
apphosting.yaml covers the deploy. The env var *replaces* the build-time list
rather than merging, which is worth knowing before editing either. The check is
that `/` contains `ng-server-context`; it went from absent to `ssg`, and the
landing page from 22,021 to 30,433 bytes with its ld+json intact.

Fixing that surfaced a second one it had been masking: `/partner-demo/` 302'd to
`/`. `express.static` runs with `index: false` — deliberately, so it cannot
pre-empt SSR of the landing page — so the directory URL matched no Angular route
and fell to the router's `**` redirect. Only `/partner-demo/index.html` worked,
while `robots.txt`, the README and `/agent` all link to `/partner-demo/`. It now
has its own static mount, `no-store` for the same reason the dev partner server
uses it.

**Deploy config states everything outright** rather than relying on framework
detection: this repo is a pnpm workspace with no framework at its root.
`rootDir: "/"` keeps pnpm-lock.yaml where the installer looks, which is the
arrangement least likely to hit firebase-tools#7478 (pnpm workspaces in a
*subdirectory*, still open). If it trips anyway nothing needs rewriting —
`node server.mjs` is a plain Node server.

`PARTNER_DEMO_ORIGIN` no longer defaults to localhost under NODE_ENV=production:
serving that from a deployed instance would make `/agent` embed an iframe
pointing at each visitor's own machine. Unset, `/agent` now says it has no second
origin and links to the partner page on this origin, instead of rendering an
empty card.

Also adds the root README the repo never had (§12 checkbox 2) — what is
WebMCP-specific and where, the flag setup, what works without it, and the deploy
steps — and replaces the stock Angular scaffold comments in `frontend/src/server.ts`,
whose commented-out `/api` block contradicts the data boundary.

Verified with NODE_ENV=production on :8080: `/` HTML with `ng-server-context`,
`/api/health` JSON, `/api/nope` a JSON 404 rather than the app shell,
`/partner-demo/` 200 and `/partner-demo` 301 to it, sign-in and a tool call
round-tripping, `/agent` listing all five tools with `approve_expense` present,
and a clean server log.

Not verified: the actual Firebase deploy. Creating the backend and the three
secrets needs an interactive Google login, so those steps are in the README's
Deploying section for a human to run. `SUPABASE_URL` in apphosting.yaml points at
the current project and should be changed for any other one.
…nd four dead routes

Everything the PRD calls Phase 0 is now built. What is left in the tracker is the
deploy itself, the demo video, and Phase 1–3 features.

**Expense actions in the UI.** The biggest hole: the Expenses page was read-only,
so `POST /expenses` via Add Expense was the only mutation a human could reach and
approvals looked like something only the Copilot could do — the wrong story for a
demo about agents and humans sharing one permission model. Every row now offers
submit / approve / reject / reimburse / reopen / delete, decisions take an
optional note inline, and delete is two-step in place.

The interesting part is where the rules live. `TRANSITION_ACTIONS`,
`TRANSITION_ROLES` and `OWNER_ONLY_ACTIONS` were backend-only, and the frontend
may not import from `backend/`, so they moved to `@actuo/shared` alongside the
legality table that was already there for exactly this reason. Both sides now go
through one `mayPerformOn`, which is what caught two bugs I had written:

  - I offered Approve on the user's *own* expense. Self-approval is a
    segregation-of-duties refusal the server has always made, so that button was
    guaranteed to 403 — precisely what deriving the UI from the server's table is
    meant to prevent. It is now `NOT_ON_OWN_ACTIONS`, read by both.
  - I hid Submit on someone else's draft. The server allows an approver to submit
    anyone's, so the UI was quietly stricter than the API. My spec had encoded
    the mistake as an assertion; both are corrected.

`backend/src/expenses/expense-state-machine.ts` keeps only the 409 mapping, which
is genuinely server-side, and re-exports the rest.

**PWA (§8.4).** `@angular/service-worker` on the production build only, a
manifest, and icons plus a 1200×630 OG card generated from the palette by
`scripts/generate-brand-assets.mjs` — committed, with the script saying where they
came from. `ngsw-config.json` declares **no `dataGroups`**: caching `/api` would
show stale money and undercut the promise that every read goes through an
authenticated route. `navigationUrls` also excludes `/partner-demo/**`, a separate
site that re-registers its WebMCP tools on load. `PwaService` holds the deferred
install prompt and online/offline; the shell renders both as banners, toned
rather than filled because `status.warning` inverts between themes.

**SEO (§8.5).** One real bug: `landing.ts` set `robots: index, follow` in its
constructor, and a meta tag is document-global, so it survived the navigation and
`/dashboard` advertised itself as indexable. Fixed structurally — `data.robots`
per route, applied by `SeoService` on every navigation, defaulting to `noindex`
for anything that declares nothing. Verified live: the tag flips going from `/`
to `/expenses`.

Absolute URLs needed a mechanism, because the public pages are prerendered and
there is no request to derive an origin from. `index.html`, `sitemap.xml` and
`robots.txt` carry a `__PUBLIC_ORIGIN__` sentinel that survives prerendering into
every generated file, and `scripts/stamp-seo.mjs` replaces it across the build
output. Unset it substitutes `''`, so a local build is never broken by an
unconfigured domain — it just stays relative.

**Four routes that existed with no caller**, each a tracker row:

  - `POST /api/budgets` — a Set a budget form for owner/admin. It inserts, and a
    unique index makes a repeat a 409, so only categories without a budget are
    listed. My first copy claimed setting one again would replace it; the API
    does not, and the form now says what is actually true. Changing an existing
    budget is still unsupported and recorded as such.
  - `audit_log` — written on every mutation since the start, read by nothing.
    Now `GET /api/audit-log` (owner/admin — it spans other people's actions) and
    a Change history panel. The tool-call panel is renamed Tool calls and both
    say what they are for: one is what changed, the other is what an agent did.
  - `untrustedContentHint` — declared and used on zero tools. Now on
    `search_expenses` and `approve_expense`, the two that surface *another
    person's* free text, and on the partner-demo tools. Shown as a badge on the
    tool-call card and the /agent panel, so it is visible rather than declared.
    `submit_expense` deliberately does not carry it: it only echoes back what the
    caller supplied in the same turn.
  - CI — `.github/workflows/ci.yml` runs the full Definition of Done gate. No
    secrets needed, which is a consequence of `EnvService` raising at call time
    rather than import time.

Verified on the production build at :8080 — the workflow controls gated
correctly per row and per role, a real approval landing in `audit_log` with its
comment, member 403s on `/audit-log` and `/budgets`, an activated service worker
with `/api` absent from `ngsw.json`, the offline banner appearing and clearing,
44px touch targets and legible contrast in both themes, and the robots tag
flipping on navigation.

Not verified: CI has never run on GitHub (`act` is not installed, so the
workflow was checked by running its command sequence locally), and the mobile tab
bar is still measured rather than seen at a real 390px viewport. Deploy is
deliberately not run — `apphosting.yaml` gains `PUBLIC_ORIGIN`, and the README
carries the backend-creation and secret commands for you.
History was rewritten to drop the `Co-Authored-By` and session-URL trailers that
had been appended to all 18 commits. Nothing enforced their absence, so the next
commit would have put them straight back and required a second rewrite.

CLAUDE.md now names the three exact trailers to never append and says plainly
that this overrides the tool's defaults, since "the tool normally does it" was
the only reason they were there. The reason is attribution, not tidiness: a
co-author trailer credits an account that is not a contributor, and GitHub
renders it on every commit and in the repo's contributor list.

The rule carves out the filename `CLAUDE.md` itself — three commits legitimately
say they changed that file, and a blanket ban would have flagged them.
`NG_ALLOWED_HOSTS` was carrying `*.hosted.app`, which is not a domain this
deploy serves from. Angular 21 checks the `Host` header against this list and,
off it, does not error — it silently falls back to client-side rendering,
discarding the SSR and structured-data work in PRD §8.5. So a wrong value here
looks like a working deploy right up until you check the HTML for
`ng-server-context`.

Replaced with the origins App Hosting actually answers on (`*.web.app`,
`*.run.app`, `*.firebaseapp.com`) plus the custom domain.
… sweep may remove

A sweep for unnecessary comments across all 177 source files found almost
nothing to take. The measurement, because it is the point: 3,850 comment lines
in 27,649, of which 5 restated the code beneath them, 0 were stale TODOs and 0
were commented-out code. What is left is 16 lines of ASCII rule with no words on
them — bare `// -----` separators, and the pair wrapped around a section title
like `// Wire types`. The titles stay; the rules around them go.

Everything else is load-bearing and stays, however verbose. `amount.ts` is 53%
prose because the reason a $200 charge must not be added to a rupee total is not
recoverable from the code, and `badge.ts` explains that `approved` is
deliberately not `success` because the next person will otherwise "fix" it.
Several comments exist precisely because the simplification was already tried
and broke something subtle — thought signatures, `parseInputSchema()`,
`setGlobalPrefix`.

So CLAUDE.md gains a Comments section stating the density is the design rather
than cleanup backlog, listing the three things a sweep may remove and the four
it must not, and giving the numbers above as a tripwire: a sweep proposing to
remove hundreds of lines has misread prose as noise.

Comments-only change. Verified with the full gate — backend 64 tests, frontend
742, `pnpm run build` clean through prerender and the SEO stamp.
@theprogrammersingh
theprogrammersingh merged commit 8e7ea29 into main Aug 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant